package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * 聊天室服务端
 */
public class Server {
    /*
        java.net.ServerSocket
        运行在服务端的ServerSocket主要任务:
        1:向服务端申请服务端口，客户端就是通过该端口与服务端建立连接的
        2:监听服务端口，一旦一个客户端连接了就会立即创建一个Socket实例与之交互
        我们将ServerSocket比喻为某客服中心的"总机"
     */
    private ServerSocket serverSocket;
    //存放所有客户端的输出流用于广播消息
    private List<PrintWriter> allOut = new ArrayList<>();

    public Server(){
        try {
            System.out.println("正在启动服务端");
            /*
                如果端口被其他程序占用了，则会抛出异常:
                java.net.BindException:address already in use
                解决办法:
                1:杀死占用该端口的进程(通常发生在服务端启动两次导致)
                2:更换端口号
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start(){
        try {
            while(true) {
                System.out.println("等待客户端连接...");
            /*
                ServerSocket提供的重要方法:
                Socket accept()
                用于接收客户端的连接，并返回一个Socket实例与连接的客户端交互。
                该方法是一个阻塞方法，调用后程序会"卡住"，直到一个客户端连接为止。
             */
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了");
                //启动一个线程来与该客户端交互
                ClientHandler handler = new ClientHandler(socket);
                Thread t = new Thread(handler);
                t.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 该线程任务是负责与指定的客户端进行交互
     */
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//记录客户端的地址信息

        public ClientHandler(Socket socket){
            this.socket = socket;
            //通过socket获取远端计算机地址信息
            host = socket.getInetAddress().getHostAddress();
        }

        public void run(){
            PrintWriter pw = null;
            try {
             /*
                    Socket提供的方法
                    InputStream getInputStream()
                    获取的字节输入流用于读取来自远端计算机发送过来的数据
                 */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流用于将消息发送给客户端
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);

                //将输出流存入共享集合用于广播消息
                synchronized (allOut) {
                    allOut.add(pw);
                }
                //广播消息通知所有客户端该用户上线了
                sendMessage(host+"上线了，当前在线人数:"+allOut.size());



                String line;
                /*
                    当我们通过BufferedReader的readLine方法读取来自远端计算机发送
                    过来一行字符串时，该方法首先会阻塞等待对方发送内容。一旦对方发送
                    过来一行字符串此时该方法会立即将这样字符串内容返回。
                    如果远端计算机断开连接:
                    远端计算机正常断开(进行了TCP挥手动作)，此时readLine方法会立即
                    返回null，表达流读取到了末尾。
                    远端计算机异常断开(没有进行TCP挥手动作,意外中止)，此时readLine
                    方法会立即抛出异常:java.net.SocketException: Connection reset

                 */
                while((line = br.readLine())!=null) {
                    sendMessage(host + "说:" + line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally{
                //处理客户端断开连接的操作
                //1将当前客户端的输出流从allOut中删除
                synchronized (allOut) {
                    allOut.remove(pw);
                }
                //2将socket关闭释放底层资源
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                //3广播该客户端下线了
                sendMessage(host+"下线了，当前在线人数:"+allOut.size());
            }
        }

        /**
         * 将消息发送给所有客户端
         * @param message
         */
        private void sendMessage(String message){
            System.out.println(message);
            //将消息发送给所有客户端
            synchronized (allOut) {
                for (PrintWriter o : allOut) {
                    o.println(message);
                }
            }
        }
    }

}






